﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Specialized;
using System.Data.SqlClient;
using System.Threading;
using CacheProviderInterfaces;
using System.Data.Common;
using Oracle.DataAccess.Client;
using System.Data;
using CacheProviderEntities;
using System.Diagnostics;
using CacheProviderUtilities;
using Microsoft.Practices.Composite.Presentation.Events;
using System.Configuration;

namespace CacheProvider
{
    public abstract class MonitoringManager<T> : IDependencyMonitoringManager where T : class
    {
        #region P R I V A T E   A T T R I B U T E S

        protected readonly CacheProvider _cacheProvider;

        protected readonly ILogWriter _logWriter;

        //Dictionary used to store key=connectionstring, value=MonitoringInfo
        //to track the connections which are being monitored.
        protected readonly HybridDictionary _monitoringDictionary;

        //used to track cache dependencies
        protected readonly List<CacheDependencyTracker> _cacheDependencyTracker;

        //Serializes access to the _cacheDependencyTracker list.  Facilitates multiple readers to simultaneously
        //access the list while guaranteeing exclusive access for only one writer at a time.
        protected static readonly ReaderWriterLockSlim DependencyTrackerLocker = new ReaderWriterLockSlim();

        //serializes access to _monitoringDictionary
        protected static readonly object DictionarySerializer = new object();

        protected int _executeRetryCount = 5;

        protected List<T> _dependenciesCollection = null;

        #endregion

        #region C O N S T R U C T  O R (S)

        public MonitoringManager(CacheProvider cacheProvider, IDependencyInjectionContainer dependencyInjectionContainer)
        {
            this._cacheProvider = cacheProvider;
            this._logWriter = dependencyInjectionContainer.Resolve<ILogWriter>();
            this._monitoringDictionary = new HybridDictionary();
            this._cacheDependencyTracker = new List<CacheDependencyTracker>();
            this._dependenciesCollection = new List<T>();
        }

        #endregion

        #region P R O T E C T E D  M E T H O D (S)

        /// <summary>
        ///     Function used to get the ConnectionString in specific
        ///     format irrespective of order of input string. The method will
        ///     use SqlConnectionStringBuilder class to format the string.
        /// </summary>
        /// <param name="connectionString"></param>
        /// <returns></returns>
        protected virtual string GetFormattedConnectionString(string connectionString)
        {
            var formattedConString = string.Empty;

            var dbConnectionStringBuilder = new DbConnectionStringBuilder
            {
                ConnectionString = connectionString
            };

            if (dbConnectionStringBuilder.ContainsKey("User ID"))
                formattedConString += dbConnectionStringBuilder["User ID"].ToString();

            if (dbConnectionStringBuilder.ContainsKey("Password"))
                formattedConString += dbConnectionStringBuilder["Password"].ToString();

            if (dbConnectionStringBuilder.ContainsKey("initial catalog"))
                formattedConString += dbConnectionStringBuilder["initial catalog"].ToString();

            if (dbConnectionStringBuilder.ContainsKey("data source"))
                formattedConString += dbConnectionStringBuilder["data source"].ToString();

            return formattedConString;
        }

        /// <summary>
        ///     
        /// </summary>
        /// <param name="connection"></param>
        /// <param name="table"></param>
        /// <returns></returns>
        protected virtual String GetSQLQuery(DbConnection connection, string table)
        {
            string[] restrictions = new string[3];

            switch (_cacheProvider.Database)
            {
                case Database.SqlServer:
                default:
                    if (table.Contains("."))
                    {
                        var tableNameParts = table.Split(new[] { '.' });

                        //Restrictions are mentioned in the following order:
                        //TABLE_CATALOG,TABLE_SCHEMA,TABLE_NAME,COLUMN_NAME
                        restrictions = new string[] { null, tableNameParts[0], tableNameParts[1], null };
                    }
                    else
                    {
                        restrictions = new string[] { null, null, table, null };
                    }
                    break;
                case Database.Oracle:
                    restrictions[1] = table;
                    break;

            }

            if (connection.State != ConnectionState.Open)
                connection.Open();

            var metadataTable = connection.GetSchema("Columns", restrictions);
            var columnNames = from row in metadataTable.AsEnumerable()
                              select row.Field<string>("COLUMN_NAME");

            var columnsForQuery = string.Join(",", columnNames.ToArray());

            var sqlQueryBuilder = new StringBuilder();
            sqlQueryBuilder.Append("SELECT ");
            sqlQueryBuilder.Append(columnsForQuery);
            sqlQueryBuilder.Append(" FROM ");
            sqlQueryBuilder.Append(table);
            return sqlQueryBuilder.ToString();
        }

        /// <summary>
        ///     
        /// </summary>
        /// <param name="regionName"></param>
        /// <param name="cacheName"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        protected virtual CacheDependencyTracker GetDependencyTracker(string regionName, string cacheName, string key)
        {
            //try
            //{
            //    DependencyTrackerLocker.EnterReadLock();

            CacheDependencyTracker dependencyTracker;

            if (this._cacheProvider.IsRegionSupported && !string.IsNullOrEmpty(regionName))
            {
                if (this._cacheProvider.IsNamedCacheSupported && !string.IsNullOrEmpty(cacheName))
                {
                    dependencyTracker = (from depInfo in this._cacheDependencyTracker
                                         where depInfo.Key.Equals(key)
                                               && depInfo.RegionName.Equals(regionName)
                                               && depInfo.NamedCache.Equals(cacheName)
                                         select depInfo).FirstOrDefault();
                }
                else
                {
                    dependencyTracker = (from depInfo in this._cacheDependencyTracker
                                         where depInfo.Key.Equals(key)
                                               && depInfo.RegionName.Equals(regionName)
                                         select depInfo).FirstOrDefault();
                }
            }
            else
            {
                if (this._cacheProvider.IsNamedCacheSupported && !string.IsNullOrEmpty(cacheName))
                {
                    dependencyTracker = (from depInfo in this._cacheDependencyTracker
                                         where depInfo.Key.Equals(key)
                                               && depInfo.NamedCache.Equals(cacheName)
                                         select depInfo).FirstOrDefault();
                }
                else
                {
                    dependencyTracker = (from depInfo in this._cacheDependencyTracker
                                         where depInfo.Key.Equals(key)
                                         select depInfo).FirstOrDefault();
                }
            }
            return dependencyTracker;
        }

        /// <summary>
        /// This method stores the dependency list in the cache.  This list, in turn, is consumed
        /// by a windows service which manages the dependencies.
        /// </summary>
        /// <param name="state"></param>
        protected virtual void EncacheDependencies(object state)
        {
            //const string dependencyTrackerCacheKey = "AppFabricCacheProvider_Admin_DependencyTracker";
            var dependencyTrackerCacheKey = this._cacheProvider.CacheClientNodeIdentifier;

            try
            {
                DependencyTrackerLocker.EnterReadLock();

                //TODO: Store it in a separate Named Cache.
                if (this._cacheDependencyTracker.Count == 0)
                    this._cacheProvider.Remove(dependencyTrackerCacheKey);
                else
                    this._cacheProvider.Put(dependencyTrackerCacheKey, this._cacheDependencyTracker);
            }
            finally
            {
                DependencyTrackerLocker.ExitReadLock();
            }
        }

        /// <summary>
        ///     Method used to remove the Depedency of an item added to cache.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="cacheName"></param>
        /// <param name="regionName"></param>
        /// <param name="serializableCacheItemVersion"></param>
        /// <returns></returns>
        protected virtual bool RemoveItemFromDependencyTracker(string key, string cacheName, string regionName, object serializableCacheItemVersion)
        {
            var isRemoved = false;

            //Find the dependency from the list
            var dependencyTracker = this.GetDependencyTracker(regionName, cacheName, key);

            if (dependencyTracker != null)
            {
                var areCacheVersionsSame = true;

                if (serializableCacheItemVersion != null)
                    areCacheVersionsSame = this._cacheProvider.IsCacheVersionSame(serializableCacheItemVersion,
                                                                                  dependencyTracker.CacheItemDataVersion);

                //If the CacheVersion is same, then windows service will automatically
                //unsubscribe all the dependency and then remove the item from the cache.
                if (areCacheVersionsSame)
                {
                    //unsubscribe the dependency notification.
                    if (dependencyTracker.DependencyData.CacheDependencyType == CacheDependencyTypes.SQLDependency)
                    {
                        RemoveChangeNotification(dependencyTracker.DependencyInstanceID);
                    }
                    //Remove it from the list.
                    isRemoved = this._cacheDependencyTracker.Remove(dependencyTracker);

                    //If dependencies are to be cached, schedule caching the same on a separate threadpool thread.
                    if (dependencyTracker.AreDependenciesCached)
                        ThreadPool.QueueUserWorkItem(EncacheDependencies);

                }
                else
                {
                    var message = string.Format("ApplicationName {0}--> Cache Item versions are not same for key {1}, Region {2} in Named Cache {3}.  Hence, not removing it from tracker!",
                            ConfigurationManager.AppSettings["ApplicationName"]
                            , key
                            , regionName
                            , cacheName);
                    if (this._logWriter != null)
                        this._logWriter.Write(message, this._cacheProvider.LoggingCategory, 1, (int)LoggingEvent.CacheNotification, TraceEventType.Critical, key);

                }
            }

            return isRemoved;
        }

        /// <summary>
        ///     
        /// </summary>
        /// <param name="state"></param>
        protected virtual void DependencyNotificationProcessingCallback(object state)
        {
            //var notificationInfo = state as SqlNotificationState;
            var notificationInfo = state as QueryNotificationState;

            if (notificationInfo == null)
                return;

            //Dispose the timer
            if (notificationInfo.Sender != null)
                notificationInfo.Sender.Dispose();

            //var e = notificationInfo.NotificationEventArgs;

            CacheDependencyTracker cacheDependencyData;

            try
            {
                DependencyTrackerLocker.EnterWriteLock();

                cacheDependencyData = this._cacheDependencyTracker.Where(tracker => tracker.DependencyInstanceID.Equals(notificationInfo.Id)).FirstOrDefault();

                if (cacheDependencyData != null)
                {
                    var message = string.Format("Processing notification for cache with key [{0}] in region [{1}] under NamedCache [{2}]",
                                                cacheDependencyData.Key, cacheDependencyData.RegionName, cacheDependencyData.NamedCache);

                    if (this._logWriter != null)
                        this._logWriter.Write(message, this._cacheProvider.LoggingCategory, 1, (int)LoggingEvent.CacheNotification, TraceEventType.Information, cacheDependencyData.Key);

                    bool isCacheEntryVersionStillSame;

                    if (this._cacheProvider.IsCacheItemVersioningSupported)
                    {
                        //If the version of the entry in cache and the one in the tracker are not same,simply return.
                        //Because, some other client has already processed in between.
                        isCacheEntryVersionStillSame =
                            this._cacheProvider.IsCacheVersionSame(cacheDependencyData.CacheItemDataVersion,
                                                                   cacheDependencyData.Key,
                                                                   cacheDependencyData.RegionName);
                    }
                    else
                        isCacheEntryVersionStillSame = true;

                    if (isCacheEntryVersionStillSame)
                    {
                        if (this._logWriter != null)
                            this._logWriter.Write("Cache Versions are still same. Hence, about to process it", this._cacheProvider.LoggingCategory, 1, (int)LoggingEvent.CacheNotification, TraceEventType.Information, cacheDependencyData.Key);

                        //Invalidate the Cache
                        var currentCacheName = string.Empty;

                        //Set Named cache if required.
                        if (this._cacheProvider.IsNamedCacheSupported)
                            currentCacheName = this._cacheProvider.CacheName;

                        if (this._cacheProvider.IsNamedCacheSupported
                            && !string.IsNullOrEmpty(cacheDependencyData.NamedCache)
                            && !currentCacheName.Equals(cacheDependencyData.NamedCache))
                        {
                            this._cacheProvider.SetNamedCache(cacheDependencyData.NamedCache);
                        }

                        //Remove Cache Entry
                        if (this._cacheProvider.IsRegionSupported
                            && !string.IsNullOrEmpty(cacheDependencyData.RegionName))
                        {
                            this._cacheProvider.Remove(cacheDependencyData.Key,
                                                       cacheDependencyData.RegionName);
                        }
                        else
                        {
                            this._cacheProvider.Remove(cacheDependencyData.Key);
                        }

                        //Restore the Named Cache (if there was one)
                        if (this._cacheProvider.IsNamedCacheSupported
                            && !string.IsNullOrEmpty(currentCacheName)
                            && !currentCacheName.Equals(cacheDependencyData.NamedCache))
                        {
                            this._cacheProvider.SetNamedCache(currentCacheName);
                        }
                    }


                    //unsubscribe the dependency notification.
                    if (cacheDependencyData.DependencyData.CacheDependencyType ==
                        CacheDependencyTypes.SQLDependency)
                    {
                        RemoveChangeNotification(cacheDependencyData.DependencyInstanceID);
                    }

                    this._cacheDependencyTracker.Remove(cacheDependencyData);

                    //If dependencies are to be cached, schedule caching the same on a separate threadpool thread.
                    if (cacheDependencyData.AreDependenciesCached)
                        ThreadPool.QueueUserWorkItem(EncacheDependencies);
                }
            }
            finally
            {
                DependencyTrackerLocker.ExitWriteLock();
            }

            if (cacheDependencyData != null && cacheDependencyData.CallbackManager != null)
                cacheDependencyData.CallbackManager.InvokeCallbacks();
        }

        /// <summary>
        /// Unregisters an already registered SqlDependency from monitoring changes.
        /// </summary>
        /// <param name="key">Cache Key with which Dependency was registered.</param>
        /// <param name="cacheName">Named Cache with which Dependency was registered, else null</param>
        /// <param name="regionName">Name of the region with which dependency was registered, else null</param>
        /// <param name="serializableCacheItemVersion"></param>
        /// <returns>True if the dependency exists and is unregistered else false</returns>
        public virtual bool RemoveSqlDependency(string key, string cacheName, string regionName, object serializableCacheItemVersion)
        {
            //Cache Entry Removal Callback processing for Classic Cache Provider happens on the same thread.  
            //In that case, the method which invokes the Cache Entry Removal and the callback from Cache store
            //happens on the same thread and are trying to hold the ReaderWriterLockSlim recursively.  
            //Hence, checking the same before acquiring the lock.

            var isWriteLockAlreadyHeldByCurrentThread = false;
            try
            {
                isWriteLockAlreadyHeldByCurrentThread = DependencyTrackerLocker.IsWriteLockHeld;
                if (!isWriteLockAlreadyHeldByCurrentThread)
                    DependencyTrackerLocker.EnterWriteLock();
                return RemoveItemFromDependencyTracker(key, cacheName, regionName, serializableCacheItemVersion);
            }
            finally
            {
                if (!isWriteLockAlreadyHeldByCurrentThread)
                    DependencyTrackerLocker.ExitWriteLock();
            }
        }

        /// <summary>
        /// Can be used to block other calls if the system is processing a notification callback so that other calls
        /// work on the latest cache state.
        /// This may result in blocking the current call if the system is processing a Data Change Notification callback.
        /// </summary>
        public virtual void UpdateDataChangeProcessingDelay(string key, string cacheName, string regionName, TimeSpan newDelay)
        {
            try
            {
                DependencyTrackerLocker.EnterWriteLock();

                var dependencyTracker = this.GetDependencyTracker(regionName, cacheName, key);

                if (dependencyTracker != null)
                {
                    dependencyTracker.DelayInDependencyNotificationProcessing = newDelay;
                }
            }
            finally
            {
                DependencyTrackerLocker.ExitWriteLock();
            }
        }

        /// <summary>
        /// Can be used to clear all DB Dependencies managed by this instance.
        /// </summary>
        public virtual void RemoveAllDependencyTrackers()
        {
            var isWriteLockAlreadyHeldByCurrentThread = false;
            //Find the dependency from the list
            try
            {
                isWriteLockAlreadyHeldByCurrentThread = DependencyTrackerLocker.IsWriteLockHeld;
                if (!isWriteLockAlreadyHeldByCurrentThread)
                    DependencyTrackerLocker.EnterWriteLock();
                if (this._cacheDependencyTracker == null)
                    return;
                foreach (var cacheDependencyTracker in _cacheDependencyTracker)
                {
                    if (cacheDependencyTracker != null)
                    {
                        //unsubscribe the dependency notification.
                        if (cacheDependencyTracker.DependencyData.CacheDependencyType == CacheDependencyTypes.SQLDependency)
                        {
                            RemoveChangeNotification(cacheDependencyTracker.DependencyInstanceID);
                        }
                    }
                }

                this._cacheDependencyTracker.Clear();
            }
            finally
            {
                if (!isWriteLockAlreadyHeldByCurrentThread)
                    DependencyTrackerLocker.ExitWriteLock();
            }
        }

        #endregion

        #region A B S T R A C T  M E T H O D (S)

        /// <summary>
        /// Sets up monitoring for Dependency Management.
        /// </summary>
        /// <param name="dbConnectionString">DB connection String for which to monitor dependencies</param>
        /// <returns>Returns <see cref="CacheDependencyStatus"/></returns>
        public abstract CacheDependencyStatus StartMonitoring(string dbConnectionString);

        /// <summary>
        /// Sets up monitoring for SqlDependency Changes.
        /// </summary>
        /// <param name="dbConnectionString">SQL Server DB connection String for which to stop monitoring dependencies</param>
        /// <returns>Returns <see cref="CacheDependencyStatus"/></returns>
        public abstract CacheDependencyStatus StopMonitoring(string dbConnectionString);

        /// <summary>
        /// Registers SqlDependencies for monitoring changes. Has provision for
        /// automatic cache updation (when dependencies change) through callback registration.
        /// </summary>
        /// <typeparam name="T">Type of Payload with which the callback should be invoked.</typeparam>
        /// <param name="key">Cache key</param>
        /// <param name="namedCache">Name of the Named cache (if any & supported)</param>
        /// <param name="regionName">If Region is supported, Name of the Region where the key\value will remain</param>
        /// <param name="dependencyInfo">An instance of <see cref="DependencyInfo" with <see cref="DependencyInfo.DependencyTypes.SQLDependency"/>/> and other suitable properties set.</param>
        /// <param name="encacheDependencies">Whether to store the dependencies in the Cache.  Useful for Distributed cache providers to provide dependency management fallback.</param>
        /// <param name="cacheUpdationCallback">The callback delegate to invoke in case dependencies change.</param>
        /// <param name="callbackState">Payload instance with which the callback should be invoked.</param>
        /// <param name="threadOption">Thread on which the callback should be invoked. See <see cref="ThreadOption"/> for details</param>
        /// <param name="keepSubscriberReferenceAlive">Should there be a strong or weak reference to the callback.</param>
        /// <param name="delayForProcessingDependencyChange">Timespan to signify delay to be introduced while processing a Dependency Change notification.  If it used
        /// so that the win Service does not process before the client(if it is alive) because the client can invoke automatic update callbacks.</param>
        /// <param name="cacheItemVersionData"></param>
        public abstract void AddSqlDependencies<T>(string key,
                                   string namedCache,
                                   string regionName,
                                   DependencyInfo dependencyInfo,
                                   bool encacheDependencies,
                                   Action<T> cacheUpdationCallback,
                                   T callbackState,
                                   ThreadOption threadOption,
                                   bool keepSubscriberReferenceAlive,
                                   TimeSpan delayForProcessingDependencyChange,
                                   Object cacheItemVersionData
                                   );

        /// <summary>
        ///     
        /// </summary>
        /// <param name="id"></param>
        public abstract void RemoveChangeNotification(string id);

        #endregion

    }
}
